package com.sanctuary.tools;
import java.security.spec.KeySpec;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.DESedeKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * Security Tools is a central location for all security related
 * functions, such as encryption/decryption of strings. As time goes
 * by this may need to be expanded to accommodate other security
 * features
 * 
 * @author David Palmer <dpalmer@molecular.com>
 * @version $Id: SecurityTools.java,v 1.1 2007/01/12 16:40:05 cvs Exp $
 *
 */
public class SecurityTools {
	/************************************
	 * Security related constants
	 ************************************/
	
	public static final String DESEDE_ENCRYPTION_SCHEME 		= "DESede";
	public static final String DES_ENCRYPTION_SCHEME 			= "DES";
	/** 
	 * Default encryption key. Please don't just use this. This is only here
	 * as as fail-safe. You really should be using a key that is more secure
	 */
	public static final String DEFAULT_ENCRYPTION_KEY			= "d41d8cd98f00b204e9800998ecf8427e";
	public static final String DEFAULT_UNICODE_FORMAT			= "UTF8";
	/**
	 * Empty constructor
	 */
	public SecurityTools() {}
	
	/**
	 * Decrypt a given string using the provided encryption scheme. Something to be aware
	 * of, for whatever reason, if you choose to use a different encryption scheme than
	 * the one you used to encrypt the input string, the decryption will fail. Its typically
	 * based to use 
	 * @param scheme - encryption scheme 
	 * @param key - encryption key
	 * @param input - the encrypted string to decrypt
	 * @param unicode - the unicode format of the string
	 * @return - the clear text of the encrypted string
	 * @throws Exception
	 */
	public String decrypt (String scheme, String key, String input, String unicode)
		throws Exception
	{
		if (unicode == null || "".equals(unicode))
			unicode = DEFAULT_UNICODE_FORMAT;
		
		byte[] keyAsBytes = key.getBytes(unicode);
		KeySpec keySpec 			= null;
		SecretKeyFactory keyFactory = null;
		Cipher cipher 				= null;
		
		try {
			keySpec = getKeySpec(scheme,keyAsBytes);
		} catch (Exception ex) {
			throw ex;
		}
		
		try {
			keyFactory = SecretKeyFactory.getInstance( scheme );
			cipher = Cipher.getInstance(scheme);
			SecretKey skey = keyFactory.generateSecret( keySpec );
			cipher.init( Cipher.DECRYPT_MODE, skey );
			BASE64Decoder base64decoder = new BASE64Decoder();
			byte[] cleartext = base64decoder.decodeBuffer( input );
			byte[] ciphertext = cipher.doFinal( cleartext );

			return StringTools.bytes2String( ciphertext );
		} catch (Exception ex) {
			throw ex;
		}
	}
	
	public String decrypt (String key, String input, String unicode) throws Exception {
		return decrypt(DES_ENCRYPTION_SCHEME,
				key,
				input,
				unicode);
	}
	
	public String decrypt(String input, String unicode) throws Exception {
		return decrypt(DES_ENCRYPTION_SCHEME,
				DEFAULT_ENCRYPTION_KEY,
				input,
				unicode);
	}
	
	public String decrypt (String input) throws Exception {
		return decrypt(DES_ENCRYPTION_SCHEME,
				DEFAULT_ENCRYPTION_KEY,
				input,
				DEFAULT_UNICODE_FORMAT);
	}
	
	/**
	 * Encrypt a string. This will encrypt the input string in the given encryption scheme. 
	 * Currently the only two schemes are provided: DESede and DES. 
	 * @param scheme - encryption scheme to use
	 * @param key - the encryption key to use
	 * @param input - the string you want encrypted
	 * @param unicode - the format of the string (unicode format - we default this, so if  you
	 * 	don't care, we'll just use the default set in @see {@link com.marley.interfaces.ToolConstants#DEFAULT_UNICODE_FORMAT} 
	 * @return - the encrypted string 
	 * @throws Exception
	 */
	public String encrypt(String scheme, String key, String input, String unicode) 
		throws Exception 
	{
		
		if (unicode == null || "".equals(unicode))
			unicode = DEFAULT_UNICODE_FORMAT;
		
		byte[] keyAsBytes = key.getBytes(unicode);
		KeySpec keySpec 			= null;
		Cipher cipher 				= null;
		SecretKeyFactory keyFactory = null;
		
		try {
			keySpec = getKeySpec(scheme,keyAsBytes);
		} catch (Exception ex) {
			throw ex;
		}
		
		keyFactory = SecretKeyFactory.getInstance( scheme );
		cipher = Cipher.getInstance(scheme);
		SecretKey skey = keyFactory.generateSecret(keySpec);
		try {
			cipher.init( Cipher.ENCRYPT_MODE, skey );
			byte[] cleartext = input.getBytes(unicode);
			byte[] ciphertext = cipher.doFinal(cleartext);

			BASE64Encoder base64encoder = new BASE64Encoder();
			return base64encoder.encode( ciphertext );
		} catch (Exception ex) {
			throw ex;
		}
		
	}
	
	/**
	 * Encrypt a string using a default (DES) encryption scheme
	 * @param key - the encryption key to use
	 * @param input - the string to encrypt
	 * @param unicode - the unicode format to use (can be left null)
	 * @return - encrypted string
	 * @throws Exception
	 */
	public String encrypt (String key, String input, String unicode) throws Exception {
		return encrypt(DES_ENCRYPTION_SCHEME,
				key,
				input,
				unicode);
	}
	
	/**
	 * Encrypt a string  using a default (DES) encryption scheme and the default
	 * encryption key (@see {@link com.marley.interfaces.ToolConstants#DEFAULT_ENCRYPTION_KEY}
	 * IT IS RECOMMENDED THAT YOU DO NOT  USE THIS FOR OBVIOUS SECURITY REASONS.
	 * I've made a relatively secure default encrypt string, a MD5 hashed string based
	 * on a file. 
	 * @param input
	 * @param unicode
	 * @return
	 * @throws Exception
	 */
	public String encrypt (String input, String unicode) throws Exception {
		return encrypt(DES_ENCRYPTION_SCHEME,
				DEFAULT_ENCRYPTION_KEY,
				input,
				unicode);
	}
	
	/**
	 * Encrypt a string using defaults for everything. Not recommended, but  useful
	 * for quick encryption without much thought
	 * 
	 * @param input
	 * @return
	 * @throws Exception
	 */
	public String encrypt (String input) throws Exception {
		return encrypt(DES_ENCRYPTION_SCHEME,
				DEFAULT_ENCRYPTION_KEY,
				input,
				DEFAULT_UNICODE_FORMAT);
	}
	
	/**
	 * Convenience method to return a KeySpec based on the scheme and the
	 * encryption key being used (as an array of bytes)
	 * 
	 * @param scheme
	 * @param keyAsBytes
	 * @return
	 * @throws Exception
	 */
	private KeySpec getKeySpec(String scheme, byte[] keyAsBytes) throws Exception
	{
		KeySpec keySpec = null;
		if ( scheme.equals( DESEDE_ENCRYPTION_SCHEME ) ) {
			keySpec = new DESedeKeySpec( keyAsBytes );
		}
		else if ( scheme.equals( DES_ENCRYPTION_SCHEME ) ) {
			keySpec = new DESKeySpec( keyAsBytes );
		} else {
			throw new IllegalArgumentException ("Scheme not supported: " + scheme);
		}
		
		return keySpec;
	}
	
	/**
	 * Decode the UID cookie. Because of the sensitive nature of the UID cookie, if
	 * you want to use it, you need to first decode it. Basically the UID cookie value
	 * has this format:
	 * 
	 * 		[md5 username]:[uid]
	 * 
	 * which is encrypted. So you need to call this method to decrypt the value, then 
	 * decode it for you, which will then pass you back a clear uid 
	 * 
	 * @param encodedString
	 * @return clear UID given a property formatted UID cookie string
	 * @throws Exception
	 */
	public String decodeUIDCookie (String encodedString) throws Exception {
		String uncString = decrypt(encodedString);
		String[] parts = uncString.split("\\:");
		String uid = parts[1];
		return uid;
	}
	public String encodeUIDCookie (String username, int uid) throws Exception {
		return encrypt(StringTools.getMD5(username) + ":" + uid);
	}
}
